/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.meta.service.impl.data.instance;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.BvtMapElement;
import org.unidata.mdm.core.type.model.GeneratingElement;
import org.unidata.mdm.core.type.model.GroupedElement;
import org.unidata.mdm.core.type.model.IndexedElement;
import org.unidata.mdm.core.type.model.LookupElement;
import org.unidata.mdm.core.type.model.ModelSearchEntry;
import org.unidata.mdm.core.type.model.NestedElement;
import org.unidata.mdm.core.type.model.PresentationElement;
import org.unidata.mdm.core.type.model.RegisterElement;
import org.unidata.mdm.core.type.model.RelationElement;
import org.unidata.mdm.core.type.model.SearchableElement;
import org.unidata.mdm.core.type.model.ValidityPeriodElement;
import org.unidata.mdm.meta.service.LookupReferenceSupport;
import org.unidata.mdm.meta.type.model.entities.LookupEntity;
import org.unidata.mdm.search.type.IndexField;
import org.unidata.mdm.system.type.support.IdentityHashSet;

/**
 * @author Mikhail Mikhailov
 *         Simple lookup entity entry wrapper.
 */
public class LookupImpl extends AbstractBvtMapImpl<LookupEntity>
    implements
        SearchableElement,
        IndexedElement,
        LookupElement,
        GeneratingElement,
        PresentationElement,
        GroupedElement,
        ValidityPeriodElement,
        LookupReferenceSupport {
    /**
     * Entry type 'REGISTER'.
     */
    private static final String ENTRY_TYPE_LOOKUP = "LOOKUP";
    /**
     * The code attribute.
     */
    private final AttributeElement code;
    /**
     * Aliases.
     */
    private final List<AttributeElement> aliases;
    /**
     * Lookups that are referenced by the lookup.
     */
    private final Map<LookupElement, Set<AttributeElement>> referencedLookups = new IdentityHashMap<>();
    /**
     * Lookups that refer to the lookup.
     */
    private final Map<LookupElement, Set<AttributeElement>> referencingLookups = new IdentityHashMap<>();
    /**
     * Entities that refers to the lookup.
     */
    private final Map<RegisterElement, Set<AttributeElement>> referencingRegisters = new IdentityHashMap<>();
    /**
     * Nested elements that refer to the lookup.
     */
    private final Map<NestedElement, Set<AttributeElement>> referencingNesteds = new IdentityHashMap<>();
    /**
     * Relation elements that refer to the lookup.
     */
    private final Map<RelationElement, Set<AttributeElement>> referencingRelations = new IdentityHashMap<>();
    /**
     * The index fields.
     */
    private final Map<String, IndexField> indexFields;
    /**
     * Autogenerated attributes.
     */
    private final List<AttributeElement> autogenerated;
    /**
     * Constructor.
     *
     * @param entity the entity
     * @param map wights map
     * @param siblings lookup entities map, keyed by name
     */
    public LookupImpl(LookupEntity entity, Map<String, Integer> map, Map<String, LookupEntity> siblings) {
        super(entity, Collections.emptyList(), map);

        this.code = getAttributes().get(entity.getCodeAttribute().getName());

        Objects.requireNonNull(this.code, "Code attribute cannot be null.");

        // Post-process lookup to lookup links
        getAttributes().values()
            .stream()
            .filter(AttributeElement::isLookupLink)
            .forEach(attr -> fixLookupLinkType(siblings.get(attr.getLookupLink().getLookupLinkName()), attr));

        this.aliases = entity.getAliasCodeAttributes().stream()
                .map(el -> getAttributes().get(el.getName()))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        this.autogenerated = getAttributes().values()
                .stream()
                .filter(AttributeElement::isGenerating)
                .collect(Collectors.toList());

        this.indexFields = MapUtils.isEmpty(getAttributes())
                ? Collections.emptyMap()
                : getAttributes().entrySet().stream()
                        .filter(e -> e.getValue().isIndexed())
                        .map(e -> Pair.of(e.getKey(), e.getValue().getIndexed()))
                        .collect(Collectors.toMap(Pair::getKey, Pair::getValue));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isLookup() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public LookupElement getLookup() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isBvtCapable() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public BvtMapElement getBvt() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isIndexed() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public IndexedElement getIndexed() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, IndexField> getIndexFields() {
        return indexFields;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isGenerating() {
        return Objects.nonNull(generator);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public GeneratingElement getGenerating() {
        return isGenerating() ? this : null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasGeneratingAttributes() {
        return CollectionUtils.isNotEmpty(autogenerated);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<AttributeElement> getGeneratingAttributes() {
        return autogenerated;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isGrouped() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public GroupedElement getGrouped() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getGroupName() {
        return getSource().getGroupName();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasPresentation() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public PresentationElement getPresentation() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isDashboardVisible() {
        return getSource().isDashboardVisible();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSearchable() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public SearchableElement getSearchable() {
        return this;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<ModelSearchEntry> getModelSearchEntries() {

        List<ModelSearchEntry> entries = new ArrayList<>();

        entries.add(new ModelSearchEntry(ENTRY_TYPE_LOOKUP, getName(), ENTRY_TYPE_LOOKUP, getName())
                .withEntryDisplayName(getDisplayName())
                .withEntryDescription(getDescription())
                .withDetail("group", getGroupName())
                .withDetail("dashboardVisible", String.valueOf(isDashboardVisible())));

        entries.addAll(getAttributeEntries(ENTRY_TYPE_LOOKUP, getName()));

        return entries;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public AttributeElement getCodeAttribute() {
        return code;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<AttributeElement> getCodeAliases() {
        return aliases;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Map<LookupElement, Set<AttributeElement>> getReferencedLookups() {
        return referencedLookups;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Map<LookupElement, Set<AttributeElement>> getReferencingLookups() {
        return referencingLookups;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Map<RegisterElement, Set<AttributeElement>> getReferencingRegisters() {
        return referencingRegisters;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Map<NestedElement, Set<AttributeElement>> getReferencingNesteds() {
        return referencingNesteds;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Map<RelationElement, Set<AttributeElement>> getReferencingRelations() {
        return referencingRelations;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getId() {
        return getName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isValidityPeriod() {
        return true;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ValidityPeriodElement getValidityPeriod() {
        return this;
    }
    /*
     * Package private post-processing support
     */
    void addReferencingLookup(LookupElement l, AttributeElement el) {
        referencingLookups
            .computeIfAbsent(l, k -> new IdentityHashSet<AttributeElement>())
            .add(el);
    }

    void addReferencedLookup(LookupElement l, AttributeElement el) {
        referencedLookups
            .computeIfAbsent(l, k -> new IdentityHashSet<AttributeElement>())
            .add(el);
    }

    void putReferencingRegisters(RegisterElement l, Set<AttributeElement> els) {
        referencingRegisters.put(l, els);
    }

    void putReferencingNesteds(NestedElement l, Set<AttributeElement> els) {
        referencingNesteds.put(l, els);
    }

    void putReferencingRelations(RelationElement l, Set<AttributeElement> els) {
        referencingRelations.put(l, els);
    }
}
